/*
 * MIT License
 *
 * Copyright (c) 2024-2025 milkpotatoes
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

const global = globalThis as { [index: string]: any };

Reflect.deleteProperty(global, "requireNapiPreview");
Reflect.deleteProperty(global, "requireInternal");

declare type UniObject<T> = { [index: symbol | string]: T }

const exchangeInnerMethod = (obj: Object, name: string): any => {
    const innerMethod = Reflect.get(obj, name);
    Reflect.deleteProperty(obj, name);
    return innerMethod;
}

const exportToGlobal = (name: string, value: any) => {
    Object.defineProperty(global, name, {
        value: value,
        enumerable: false,
        configurable: true,
        writable: true,
    });
}

const exportsToGlobal = (values: Iterable<[string, any]>) => {
    for (let [k, v] of values) {
        exportToGlobal(k, v);
    }
}

const repeatStr = (str: string, size: number) => {
    let result = '';
    for (let i = 0; i < size; i++) {
        result += str;
    }
    return result;
}

{
    const proc = process as { [index: string]: any };
    let g_processExitCode: any;
    const setExitCodeFunc: (code: number) => void = exchangeInnerMethod(proc, "_setExitCode");
    Object.defineProperty(process, "exitCode", {
        get() {
            return g_processExitCode;
        },
        set(v: number) {
            g_processExitCode = v;
            setExitCodeFunc(v);
        },
    });

    const innerKill: (pid: number, signal: number) => number = exchangeInnerMethod(proc, "_innerKill");
    const signalMaps: { [index: string]: number } = exchangeInnerMethod(proc, "_signalMaps");

    proc.kill = function kill(pid: number, signal?: number | string) {
        if (arguments.length === 0) {
            throw new RangeError(`Failed to execute 'kill': 1 argument required, but only ${arguments.length} present.`);
        }
        if (typeof pid !== 'number') {
            throw new TypeError("Invalid arguments, paramter pid must be number, but " + typeof pid + " is provided");
        }
        if (arguments.length == 1) {
            return innerKill(pid, signalMaps.SIGTERM);
        }
        const sigType = typeof signal;
        if (sigType === 'number') {
            return innerKill(pid, signal as number);
        }
        if (sigType === 'string') {
            if (!signalMaps.hasOwnProperty(signal as string)) {
                throw new TypeError("Invalid arguments, paramter signal is not invalid SIGNAL");
            }
            return innerKill(pid, signalMaps[signal as string]);
        }
        throw new TypeError("Invalid arguments, paramter signal must be number or string, but " + sigType + " is provided");
    }

    const innerGetEnviron: () => string[] = exchangeInnerMethod(proc, "_innerGetEnviron");
    const innerGetEnv: (name: string) => string | undefined = exchangeInnerMethod(proc, "_innerGetEnv");
    const innerSetEnv: (name: string, value: string) => number = exchangeInnerMethod(proc, "_innerSetEnv");
    const originEnviron: UniObject<string> = {};
    const getCurrentEnv = () => {
        const result: UniObject<string> = {};
        (innerGetEnviron() as string[]).map((e) => {
            const [k, v] = e.split("=", 1);
            result[k] = v;
        });
        return result;
    }

    const innerUnsetEnv: (name: string) => number = exchangeInnerMethod(proc, "_innerUnsetEnv");
    const envProxy = new Proxy(originEnviron, {
        defineProperty(_target, _property, _attributes) {
            throw Error('env Object dose not support defineProperty.');
        },
        deleteProperty(_target, p) {
            if (typeof p === 'symbol') {
                throw new Error('env Object dose not support symbol.');
            }
            innerUnsetEnv(p);
            return true;
        },
        get(_target, p, _receiver) {
            if (typeof p === 'symbol') {
                throw new Error('env Object dose not support symbol.');
            }
            if (p === "valueOf") {
                return () => {
                    return Object.valueOf.apply(getCurrentEnv());
                }
            }
            if (p === "toString") {
                return () => {
                    return innerGetEnviron().join("\n");
                }
            }
            return innerGetEnv(p);
        },
        set(_target, p, newValue, _receiver) {
            if (typeof p == "symbol") {
                throw new Error('env Object dose not support symbol.');
            }
            if (typeof newValue !== 'string') {
                throw new Error('env value must be type of string.');
            }
            return innerSetEnv(p, newValue) === 0;
        },
        has(_target, p) {
            if (typeof p == "symbol") {
                throw new Error('env Object dose not support symbol.');
            }
            return innerGetEnv(p) !== void 0;
        },
        ownKeys(_target) {
            return Object.keys(getCurrentEnv());
        },
    });
    proc.env = envProxy;

    const innerGetTitle: () => string = exchangeInnerMethod(proc, "_innerGetTitle")
    const innerSetTitle: (title: string) => void = exchangeInnerMethod(proc, "_innerSetTitle")
    Reflect.defineProperty(proc, "title", {
        get() {
            return innerGetTitle();
        },
        set(v: string) {
            innerSetTitle(v);
        },
    });
}

{
    Object.defineProperties(File, {
        SEEK_SET: {
            value: 0,
            configurable: false,
            writable: false,
            enumerable: false,
        },
        SEEK_CUR: {
            value: 1,
            configurable: false,
            writable: false,
            enumerable: false,
        },
        SEEK_END: {
            value: 2,
            configurable: false,
            writable: false,
            enumerable: false,
        },
    });
}

{
    const timerList: Map<string, bigint> = new Map();

    const NS_TO_MS = 10n ** 6n;
    const MIN_NS_HEADER_LEN = String(NS_TO_MS).length;

    const innerTimeInNs: () => bigint = exchangeInnerMethod(console, "_innerTimeInNs");

    console.time = function time(label: any = 'default') {
        if (typeof label != "string") {
            label = String(label);
        }

        if (timerList.has(label)) {
            console.warn(`Timer '${label}' already exists`);
            return;
        }

        const start = innerTimeInNs();
        timerList.set(label, start);
    }

    console.timeEnd = function timeEnd(label: string = 'default') {
        const end = innerTimeInNs();
        if (typeof label != "string") {
            label = String(label);
        }

        const start = timerList.get(label)!;
        if (start === void 0) {
            console.warn(`Timer '${label}' does not exist`);
            return;
        }

        let cost = end - start;
        let ms = cost / NS_TO_MS;
        const needFill = MIN_NS_HEADER_LEN - String(cost).length;
        while (cost % 10n === 0n) cost /= 10n;
        console.info(`${label}: ${ms}.${repeatStr('0', needFill)}${String(cost)} ms`);
        timerList.delete(label);
    }

    console.timeLog = function timeLog(label: string = 'default', ...data: any[]) {
        const end = innerTimeInNs();
        if (typeof label != "string") {
            label = String(label);
        }

        const start = timerList.get(label)!;
        if (start === void 0) {
            console.warn(`Timer '${label}' does not exist`);
            return;
        }

        let cost = end - start;
        let ms = cost / NS_TO_MS;
        const needFill = MIN_NS_HEADER_LEN - String(cost).length;
        while (cost % 10n === 0n) cost /= 10n;
        console.info(`${label}: ${ms}.${repeatStr('0', needFill)}${String(cost)} ms`, ...data);
    }

    console.assert = function assert(arg: boolean, ...msg: any[]) {
        if (!arg) {
            if (msg.length > 0) {
                console.error('Assertion Failed:', ...msg);
            } else {
                console.error('Assertion Failed');
            }
        }
    }
}

{
    enum RequireBaseDir {
        DEFAULT = 0,
        WORK_DIR = 1 << 0,
        SCRIPT_DIR = 1 << 1,
        BUILD_DIR = 1 << 2,
        EXECUTABLE_DIR = 1 << 3,
    };

    const nativeRequireNapi = exchangeInnerMethod(global, "requireNapi");
    const nativePreLoad = exchangeInnerMethod(global, "preLoad");
    const buildDir = exchangeInnerMethod(global, "_buildDir");

    const getFileAtStack = (stack: string): string => {
        const strs = stack.split('(');
        return strs[strs.length - 1].split(':')[0];
    }

    const getParent = (path: string): string => {
        const basePath = File.realPath(path);
        if (basePath === undefined) {
            return path;
        }
        const parent = basePath.substring(0, basePath.lastIndexOf('/') + 1);
        return parent;
    }

    const getAbsolutePath = (baseDir: string, file: string): string => {
        const absPath = File.realPath((baseDir.endsWith('/') ? baseDir : (baseDir + '/')) + file);
        return absPath ?? file;
    }

    const getNativeModulePath = (path: string, baseDir: RequireBaseDir | string): string => {
        if (path.startsWith('/')) {
            return path;
        }

        if (typeof baseDir === 'string') return getAbsolutePath(baseDir, path);

        if ((baseDir & RequireBaseDir.SCRIPT_DIR) !== 0) {
            const requireStack = new Error().stack!.split('\n')[2]!; // 2: skip current file and calling file
            const requireFile = File.realPath(getFileAtStack(requireStack));
            if (requireFile === undefined) return path;
            return getAbsolutePath(getParent(requireFile), path);
        }
        if ((baseDir & RequireBaseDir.WORK_DIR) !== 0) return getAbsolutePath(process.cwd(), path);
        if ((baseDir & RequireBaseDir.EXECUTABLE_DIR) !== 0) return getAbsolutePath(getParent(process.execPath), path);
        if ((baseDir & RequireBaseDir.BUILD_DIR) !== 0) return getAbsolutePath(buildDir, path);
        return path;
    }

    const defaultRequirePath = RequireBaseDir.WORK_DIR | RequireBaseDir.SCRIPT_DIR;
    function requireNapi(path: string, baseDir: RequireBaseDir | string = RequireBaseDir.DEFAULT): any {
        return nativeRequireNapi(getNativeModulePath(path,
            baseDir === RequireBaseDir.DEFAULT ? defaultRequirePath : baseDir));
    }

    function preLoad(path: string, baseDir: RequireBaseDir | string = RequireBaseDir.DEFAULT): void {
        return nativePreLoad(getNativeModulePath(path,
            baseDir === RequireBaseDir.DEFAULT ? defaultRequirePath : baseDir));
    }

    exportsToGlobal(
        [
            ["requireNapi", requireNapi],
            ["preLoad", preLoad],
            ["RequireBaseDir", RequireBaseDir],
        ]
    );
}

export function queueMicrotask(task: () => void) {
    if (arguments.length < 1) {
        throw new TypeError("Failed to execute 'queueMicrotask' on 'global': " +
            "1 argument required, but only 0 present.");
    }
    if (typeof task !== 'function') {
        throw new TypeError("Failed to execute 'queueMicrotask' on 'global': " +
            "parameter 1 is not of type 'Function'.");
    }
    new Promise<void>((r) => r()).then(task);
}
global.queueMicrotask = queueMicrotask;
